package com.springboot.frame.web.captcha;

import com.springboot.frame.web.captcha.exception.CaptchaErrorException;
import com.springboot.frame.web.captcha.exception.CaptchaException;
import com.springboot.frame.web.captcha.exception.CaptchaTimeOutException;
import com.springboot.frame.web.util.WebUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
import static com.springboot.frame.web.util.Constants.CAPTCHA_SESSION_CODE;
import static com.springboot.frame.web.util.Constants.CAPTCHA_SESSION_DATE;


/**
 * 默认实现验证码
 *
 * @author madman
 */
@Slf4j
public class DefCaptcha implements Captcha {

    private CaptchaConfig config;

    public DefCaptcha(CaptchaConfig captchaConfig) {
        this.config = captchaConfig;
    }

    /**
     * 可重写渲染方法
     *
     * @return
     */
    @Override
    public String render() {
        return render(config.getWidth(), config.getHeight());
    }

    @Override
    public String render(int width, int height) {
        HttpServletResponse response = WebUtils.response();
        response.setDateHeader(HttpHeaders.EXPIRES, 0L);
        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
        response.addHeader(HttpHeaders.CACHE_CONTROL, "post-check=0, pre-check=0");
        response.setHeader(HttpHeaders.PRAGMA, "no-cache");
        response.setContentType("image/jpeg");

        Random random = ThreadLocalRandom.current();
        StringBuilder captcha = null;
        String source = config.getSource();
        Integer length = config.getLength();
        String reg = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{" + length + "}$";
        do {
            captcha = new StringBuilder();
            for (int i = 0; i < length; i++) {
                captcha.append(source.charAt(random.nextInt(source.length() - 1)));
            }
        } while (!Pattern.matches(reg, captcha.toString()));

        try (ServletOutputStream out = response.getOutputStream()) {

            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D graphics = image.createGraphics();
            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            // 设置边框色
            graphics.setColor(Color.GRAY);
            graphics.fillRect(0, 0, width, height);

            // 设置背景色
            Color c = randColor(200, 250);
            graphics.setColor(c);
            graphics.fillRect(0, 2, width, height - 4);

            //绘制干扰线
            graphics.setColor(randColor(160, 200));
            for (int i = 0; i < 20; i++) {
                int x = random.nextInt(width - 1);
                int y = random.nextInt(height - 1);
                int xl = random.nextInt(6) + 1;
                int yl = random.nextInt(12) + 1;
                graphics.drawLine(x, y, x + xl + 40, y + yl + 20);
            }

            // 添加噪点
            float noiseRate = config.getNoiseRate();
            int area = (int) (noiseRate * width * height);
            for (int i = 0; i < area; i++) {
                int x = random.nextInt(width);
                int y = random.nextInt(height);
                int rgb = randomIntColor();
                image.setRGB(x, y, rgb);
            }

            //扭曲图片
            shear(graphics, width, height, c);

            graphics.setColor(randColor(100, 160));
            int fontSize = height - 4;
            Font font = new Font("Algerian", Font.ITALIC, fontSize);
            graphics.setFont(font);
            char[] chars = captcha.toString().toCharArray();
            for (int i = 0; i < length; i++) {
                AffineTransform affine = new AffineTransform();
                affine.setToRotation(Math.PI / 4 * random.nextDouble() * (random.nextBoolean() ? 1 : -1),
                        (width / length) * i + fontSize / 2,
                        height / 2);
                graphics.setTransform(affine);
                graphics.drawChars(chars, i, 1, ((width - 10) / length) * i + 5,
                        height / 2 + fontSize / 2 - 10);
            }
            graphics.dispose();
            HttpSession session = WebUtils.session();
            session.setAttribute(CAPTCHA_SESSION_CODE, captcha.toString());
            session.setAttribute(CAPTCHA_SESSION_DATE, Instant.now());

            ImageIO.write(image, "jpg", out);
            return captcha.toString();
        } catch (IOException e) {
            throw new CaptchaException("验证码生成出错", e);
        }
    }

    @Override
    public boolean validate(String captcha) {
        return validate(captcha, config.getTimeOut());
    }

    @Override
    public boolean validate(String captcha, int second) {
        if (StringUtils.isNotBlank(captcha)) {
            HttpSession httpSession = WebUtils.request().getSession(false);
            if (httpSession != null) {
                String sessionCaptcha = (String) httpSession.getAttribute(CAPTCHA_SESSION_CODE);
                if (StringUtils.equalsIgnoreCase(sessionCaptcha, captcha)) {
                    Instant instant = (Instant) httpSession.getAttribute(CAPTCHA_SESSION_DATE);
                    long duration = Duration.between(instant, Instant.now()).getSeconds();
                    if (duration < second) {
                        httpSession.removeAttribute(CAPTCHA_SESSION_CODE);
                        httpSession.removeAttribute(CAPTCHA_SESSION_DATE);
                        return true;
                    } else {
                        throw new CaptchaTimeOutException();
                    }
                } else {
                    throw new CaptchaErrorException();
                }
            } else {
                throw new CaptchaTimeOutException();
            }
        } else {
            throw new CaptchaErrorException();
        }
    }

    private Color randColor(int fc, int bc) {
        Random random = ThreadLocalRandom.current();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    private int randomIntColor() {
        Random random = ThreadLocalRandom.current();
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private void shearX(Graphics g, int w1, int h1, Color color) {
        Random random = ThreadLocalRandom.current();
        int period = random.nextInt(2);
        int frames = 1;
        int phase = random.nextInt(2);
        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            g.setColor(color);
            g.drawLine((int) d, i, 0, i);
            g.drawLine((int) d + w1, i, w1, i);
        }
    }

    private void shearY(Graphics g, int w1, int h1, Color color) {
        Random random = ThreadLocalRandom.current();
        int period = random.nextInt(40) + 10;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            g.setColor(color);
            g.drawLine(i, (int) d, i, 0);
            g.drawLine(i, (int) d + h1, i, h1);
        }
    }

}
